Genesis 3D

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Corso sull'engine 3D di Genesis

Decima lezione

di Luca Sabatucci

Per commentare, dare suggerimenti o consigli (molto apprezzati) e per segnalare eventuali errori o
disattenzioni (sempre possibili), puoi inviare una email all'autore (clicca sul nome sopra). Grazie per la collaborazione!


Programmazione: parte I

Come creare un progetto con Visual C++ 6.0 per utilizzare le librerie genesis 3D.

In questo tutorial creiamo il primo programma che utilizza le librerie Genesis per aprire una finestra in fullscreen, caricare una mappa e orientare la telecamera verso un punto.

Creare un nuovo progetto

La prima cosa da fare per creare un progetto con Visual C++ 6.0 e creare una sottocartella nella propria area di lavoro nella quale copiare le directory "lib", "include" presenti nella directory Genesis, e creare due nuove cartelle: "levels" e "actor".

Copiare i file .dll presenti nella directory genesis nella cartella creata. Copiare il file .bsp relativo alla mappa che è stata creata nella directory levels.

La struttura finale della nostra cartella "tesina" sarà:

 

 

Apriamo Visual C++ e creiamo un nuovo progetto di tipo "Win 32 Application", posizioniamo il progetto nella directory appena creata e diamo il nome desiderato.

Alla domanda successiva rispondere "Empty project".

 

Il wizard ci presenterà un progetto vuoto. La prima cosa da fare è quella di includere le librerie di genesis. Scegliere "Project" "Add to Project" e "Files". Selezionare la cartella Lib e cambiare il filtro in modo da visualizzare i file .lib; selezionare il file genesis.lib e confermare.

 

Adesso dobbiamo creare un nuovo file .c; a tal proposito bisogna subito fare una precisazione. Le librerie Genesis sono scritte in C e il VC++ compila in modo diverso file di tipo .cpp e file di tipo .c; per non avere alcun problema di compilazione è meglio utilizzare il linguaggio C.

Visual C++ non permette di creare file .c, quindi questi dovranno essere creati esternamente e poi inclusi (con la stessa procedura con cui abbiamo incluso le librerie).

 

Creiamo un file main.c e un file main.h e importiamoli nel progetto.

 

Nel file main.c aggiungere le righe:

 

#include <windows.h>

#include "include\genesis.h"

 

#include "main.h"

 

La prima serve per utilizzare le strutture e le funzioni di windows per aprire una finestra. La seconda informa il compilatore che i prototipi delle strutture e delle funzioni genesis si trovano nella directory include nel file genesis.h.

La terza riga fa si che nel file main.h si possano mettere strutture dati e prototipi relativi al progetto.

Il file main.h

 

#ifndef MAIN_H

#define MAIN_H

 

//Includes

#include <windows.h>         //Always include this before genesis.h

#include "include\genesis.h"       //The Genesis SDK include header

 

enum stato_giocatore { standing, walinkg, running, jumping, jumping_down, falling };

enum direzione { UP, FRONT, LEFT };

 

A parte le direttive di inclusione che sono simili a quelle appena viste, i due tipi enumerativi appena creati serviranno in seguito, quando faremo muovere la telecamera nell'ambiente. Il primo indica lo stato del giocatore, ovvero se stiamo camminando, correndo, saltando ecc...

La seconda serve per specificare un vettore. Vedremo poi.

 

typedef struct {

      char driver;            //Driver we want to use

      int   Width;            //Width of the mode

      int   Height;           //Height of the mode

      int   keys[256];        // Array Used For The Keyboard Routine

} InputOutput;

 

typedef struct {

      geVec3d     posizione;        // Posizione del giocatore

      geVec3d     angolo;           // direzione sguardo

 

      geVec3d     Mins;             // Bounding box

      geVec3d     Maxs;            

 

      int   height;                 //how tall are we

      int   speed;                  //how fast are we

      int   caduta;                 // velocità di caduta

      int salto;                   // altezza del salto

      int gradino;                 // altezza massima del gradino che può salire

 

      int stato;

      int counter;

} Player;

 

// global definition

Player            pl;         // variabile relativa al giocatore

InputOutput       io;         // parametri usati per input/output

 

Queste due strutture dati, di cui creiamo una istanza contengono informazioni essenziali all'animazione. Player contiene i parametri del giocatore, tipo ingombro e velocità nella corsa. InputOutput contiene informazioni sulla modalità video e sullo stato della tastiera.

 

HWND              hWnd;             //The window

geEngine          *Engine;          //The engine object

 

geSound_System    *SoundSys;        //The Genesis Sound System

 

geCamera    *Camera;          //The camera object

geWorld     *World;           //World Object

 

Queste variabili globali servono per la gestione dell'engine di genesis.

 

// main headers

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd);

void CreateMainWindow(HINSTANCE hInstance, int nShowCmd);

void Shutdown();

 

void MainLoop();

 

geBoolean InitEngine(HWND hWnd);

void FindDriver(geDriver_System *DrvSys);

void LoadPrefs(char *drv, int *width, int *height);

void LoadLevel(char *FileName);

 

#endif

 

Infine sono elencati i prototipi delle funzioni che andremo a creare nel file main.c.

La funzione main()

La funzione main risulta ottima per capirei passi della creazione dell'applicazione.

 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)

{

      LoadPrefs(&io.driver, &io.Width, &io.Height);

      CreateMainWindow(hInstance,nShowCmd);

     

      //Initialize the Genesis engine

      InitEngine(hWnd);

 

      MainLoop();

 

      //Shutdown the Genesis engine and clean up the memory     

      Shutdown();

     

      //End Program

      return 1;

}

 

La prima cosa che viene fatta è quella di leggere da un file di configurazione qual'è la modalità video prescelta. In questo modo l'applicazione può essere configurata esternamente.

 

La seconda istruzione crea la finestra entro la quale deve girare l'applicazione genesis.

 

InitEngine è la funzione della quale parleremo più in dettaglio in questa lezione. Essa si occupa di inizializzare il motore grafico.

 

MainLoop è una funzione che effettua il rendering grafico dell'ambiente.

 

Shutdown di occupa di deallocare le risorse impiegate in modo da poter terminare correttamente il programma.

La funzione LoadPrefs()

Tale funzione presume che nella directory principale del progetto sia presente un file di configurazione in formato txt chiamato "video.cfg". Il file deve presentare tre righe. Nella prima va specificato il driver da usare, nelle altre due la risoluzione.

Il driver deve essere specificato nel formato seguente:

( = D3D
G = Glide
S = Software

 

Un esempio di un possibile contenuto del file è:

(

640

480

 

che indica che si usa il driver per le DirectX con la modalità video 640 x 480.

 

void LoadPrefs(char *drv, int *width, int *height)

{

      FILE        *f;

 

      //Open the minapp.cfg file

      if ((f = fopen("video.cfg", "r+")) == NULL)

      {

            MessageBox(hWnd, "Failed to read viedo.cfg", "No Prefs", MB_OK);

            _exit(-1);

      }

     

      //read in the driver

      fscanf(f, "%s", drv);

 

      //read in the width

      fscanf(f, "%d", width);

 

      //read in the height

      fscanf(f, "%d", height);

      //close the file

      fclose(f);

}

 

La funzione è abbastanza semplice: i valori vengono letti e passati per riferimento alla funzione chiamante.

La funzione CreateMainWnd()

Non mi soffermerò sul significato delle istruzioni presenti in questa funzione, in quanto esula dallo scopo di questo tutorial. Consultare un manuale sulla programmazione delle win32 per una maggiore comprensione.

 

void CreateMainWindow(HINSTANCE hInstance, int nShowCmd) {

      WNDCLASS                wc;

      DWORD       dwExStyle;              // Window Extended Style

      DWORD       dwStyle;                // Window Style

      RECT        WindowRect;             // Grabs Rectangle Upper Left / Lower Right Values

      WindowRect.left=(long)0;                 // Set Left Value To 0

      WindowRect.right=(long)io.Width;               // Set Right Value To Requested Width

      WindowRect.top=(long)0;                  // Set Top Value To 0

      WindowRect.bottom=(long)io.Height;             // Set Bottom Value To Requested Height

 

      //Initialize the window class

      hInstance         = GetModuleHandle(NULL);                       // Grab An Instance For Our Window

      wc.style          = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;                // Redraw On Size, And Own DC For Window.

      wc.lpfnWndProc          = (WNDPROC) WndProc;                           // WndProc Handles Messages

      wc.cbClsExtra           = 0;                                     // No Extra Window Data

      wc.cbWndExtra           = 0;                                     // No Extra Window Data

      wc.hInstance            = hInstance;                                   // Set The Instance

      wc.hCursor        = LoadCursor(NULL, IDC_ARROW);                       // Load The Arrow Pointer

      wc.lpszMenuName         = NULL;                                        // We Don't Want A Menu

      wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

      wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);

      wc.lpszClassName = "MinApp";

 

      //Register the window class

      RegisterClass(&wc);

     

      dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;              // Window Extended Style

      dwStyle=WS_OVERLAPPEDWINDOW;                         // Windows Style

 

      AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);            // Adjust Window To True Requested Size

 

      //Create the window

      hWnd=CreateWindowEx(    dwExStyle,              // Extended Style For The Window

                                         wc.lpszClassName,

                                         "Tesina di Informatica Grafica",

                                         dwStyle |               // Defined Window Style

                                         WS_CLIPSIBLINGS |       // Required Window Style

                                         WS_CLIPCHILDREN,        // Required Window Style

                                         0,

                                         0,                                 // Window Position

                                         WindowRect.right-WindowRect.left,  // Calculate Window Width

                                         WindowRect.bottom-WindowRect.top,  // Calculate Window Height

                                         NULL,                   // No Parent Window

                                         NULL,                   // No Menu

                                         hInstance,              // Instance

                                         NULL);                       // Dont Pass Anything To WM_CREATE

 

 

      if (!hWnd)

      {

            MessageBox(NULL, "Could not create main window!", "MinApp Error...", 48);

            _exit(-1);

      }

 

      //Make the window appear and update it

      ShowWindow(hWnd, nShowCmd);

      UpdateWindow(hWnd);

 

}

 

L'unica cosa da tenere in conto è il puntatore hWnd alla struttura HWND di windows che serve come "handler" della viewport che abbiamo creato. Questo parametro sarà necessario per inizializzare il motore grafico.

La funzione WndProc()

Mediante questa funzione rileviamo i messaggi di windows che ci interessano. Nel nostro caso questa funzione ci servirà per la lettura della tastiera. A tale scopo abbiamo un vettore di 256 elementi con il quale mappiamo i tasti. Ogni elemento riposta lo stato del tasto. TRUE = premuto, FALSE = rilasciato.

 

LRESULT CALLBACK WndProc(    HWND  hWnd,             // Handle For This Window

                                         UINT  uMsg,             // Message For This Window

                                         WPARAM      wParam,                 // Additional Message Information

                                         LPARAM      lParam)                 // Additional Message Information

{

      switch (uMsg){                                                   // Check For Windows Messages

            case WM_CLOSE:                                             // Did We Receive A Close Message?

            {

                  PostQuitMessage(0);                                  // Send A Quit Message

                  return 0;                                            // Jump Back

            }

 

            case WM_KEYDOWN:                                     // Is A Key Being Held Down?

            {

                  io.keys[wParam] = TRUE;                        // If So, Mark It As TRUE

                  return 0;                                            // Jump Back

            }

 

            case WM_KEYUP:                                             // Has A Key Been Released?

            {

                  io.keys[wParam] = FALSE;                             // If So, Mark It As FALSE

                  return 0;                                            // Jump Back

            }

      }

 

      // Pass All Unhandled Messages To DefWindowProc

      return DefWindowProc(hWnd,uMsg,wParam,lParam);

}

La funzione InitEngine()

Come abbiamo annunciato questa è la funzione sulla quale ci soffermeremo più a lungo. Non è particolarmente complicata, ma da modo di spiegare alcuni concetti base di genesis.

 

geBoolean InitEngine(HWND hWnd)

{

      GE_Rect     Rect;                   // rettangolo di viewport

      geDriver_System *DrvSys;           // driver di sistema

      geXForm3d ViewXForm;

 

      Engine = geEngine_Create(hWnd, "Tesina", ".");

      if (!Engine)

      {

            MessageBox(hWnd, "Could not create engine object!", "MinApp Error...", 48);

            return GE_FALSE;

      }

 

Ecco qui l'istruzione più importante di tutto il programma. La creazione dell'engine, ovvero il motore grafico. Engine è un puntatore alla struttura geEngine, che funge da wrappler per tutte le altre stutture di genesis.

Ecco come si pronuncia la documentazione originale di genesis in proposito:

"The geEngine object is the container for the geDriver_System object, the geWorld object, and the geBitmap object. The primary job of the geEngine object is to provide a fast, and efficient interface to the output device, via the geDriver_System object."

In sostanza geEngine ci permette di manipolare oggetti come il World (che comprende la geometria dell'ambiente, degli actors, le telecamere, le luci ecc...) i driver e gli oggetti 2D.

 

      geEngine_EnableFrameRateCounter(Engine, FALSE);

 

Questa istruzione abilita o disabilita la visualizzazione dei parametri di rendering. Può essere utile attivarla per capire il concetto.

           

      //This function will enable you to use sound with your program through the Genesis API.

      SoundSys = geSound_CreateSoundSystem(hWnd);

      if (!SoundSys)

            MessageBox(hWnd, "Could not create Sound System!  There will be no sound!", "No Sound...", 48);

 

Creiamo il driver per il suono.

           

      DrvSys = geEngine_GetDriverSystem(Engine);

      if (!DrvSys)

      {

            MessageBox(hWnd, "Could not get the Genesis Driver System!", "MinApp Error...", 48);

            return GE_FALSE;

      }

 

Adesso che abbiamo un motore grafico dobbiamo settare i suoi parametri di funzionamento. La prima cosa da fare è scegliere il driver da utilizzare. A tale scopo si istanzia la variabile DrvSys che serve da manipolatore del driver scelto. Quindi con la funzione FindDrivers si impostano driver e modalità video secondo i parametri che abbiamo letto dal file di configurazione, e secondo le disponibilità del sistema.

 

      //Set the driver you want to use

      FindDriver(DrvSys);

     

      //Setup the camera rect

      Rect.Left = 0;                     //The far left of the screen

      Rect.Right = io.Width - 1;         //The far right of the screen

      Rect.Top = 0;                      //The very top of the screen

      Rect.Bottom = io.Height - 1; //The very bottom of the screen

 

      //Create the camera object.  The first parameter is the FOV (Field of View).  2.0f represents a 90

      //degree FOV.  The second is the rectangle we just setup.

      Camera = geCamera_Create(2.0f, &Rect);

     

      //We can't work without a camera.  If it doesn't exist, exit the program

      if (!Camera)

      {

            MessageBox(hWnd, "Could not create camera object!", "MinApp Error...", 48);

            _exit(-1);

      }

 

Adesso creiamo una telecamera. I parametri per la creazione sono: FOV (field of view), ovvero l'angolo di vista (2.0f corrisponde a circa 90 gradi) e Rect che è il rettangolo di viewport (in coordinate di schermo). Il tipo di proiezione della telecamera è di tipo prospettico.

 

      //Set up the XForm for the camera.  XForms determine your place in the world.

      //Clear the transform

      geXForm3d_SetIdentity(&ViewXForm);

 

      //Setup the rotation

      geXForm3d_RotateX(&ViewXForm, 0.0f);

      geXForm3d_RotateY(&ViewXForm, 0.0f);

      geXForm3d_RotateZ(&ViewXForm, 0.0f);

 

      geXForm3d_Translate(&ViewXForm, 0.0f, 180.0f, 0.0f);

 

      //Set the XForm to the camera

      geCamera_SetWorldSpaceXForm(Camera, &ViewXForm);

 

Adesso determiniamo la posizione della telecamera. Per farlo analizziamo un utile strumento di genesis: geXForm3d.

Dalle specifiche leggiamo che "geXForm3d is a transform object. It is used to specify orientations for objects in a coordinate system, and to transform world coordinates to screen coordinates for rendering."

Si tratta del wrappler di una matrice di trasformazione. Impostiamo la matrice come identità e poi sommiamo la rotazione e la traslazione.

 

Con il comando geCamera_SetWorldSpaceXForm applichiamo la trasformazione risultante alla camera.

 

      //Load the level

      LoadLevel("levels\\esempio.bsp");

 

      return GE_TRUE;

}

 

L'ultima operazione è quella di creare un oggetto geWorld, demandato alla funzione LoadLevel(). Il parametro passato è una stringa con la posizione del file della mappa da caricare.

La funzione FindDriver()

Questa funzione si occupa di cercare il miglior driver che il sistema ha a disposizione e che risponda alle caratteristiche selezionate dall'utente nel file di configurazione.

 

void FindDriver(geDriver_System *DrvSys) {

      char              *drvname;

      int               tempwidth;

      int               tempheight;

      geDriver          *Driver;

      geDriver_Mode     *Mode;

 

      //Get the first driver

      Driver = geDriver_SystemGetNextDriver(DrvSys, NULL);

 

Mediante l'istruzione geDriver_SystemGetNextDriver è possibile scorrere i driver a disposizione. I driver sono rappresentati dai file .dll presenti nella directory principale del sistema e sono di tre tipi: DirectX, Glide e Software.

 

      //Loop through the driver list and find the driver that matches what we loaded into our variables before.

      while (1)

      {

            //If there is no first driver then break the loop

            if (!Driver)

            {

                  MessageBox(hWnd, "Could not find a valid driver!", "MinApp Error...", 48);

                  _exit(-1);

            }

 

            //Get the name of the driver

            geDriver_GetName(Driver, &drvname);

 

            //Check and see if the first character matches the driver we loaded into our variable before.  If

            //it is, then break the loop.

            if (drvname[0] == io.driver)

                  break;

 

            //If not, get the next one

            Driver = geDriver_SystemGetNextDriver(DrvSys, Driver);

      }

 

Mediante questo loop scorriamo tutti i driver di sistema e cerchiamo quello il cui nome corrisponde a quello settato nel file di configurazione ('(', 'G', S'). Se nessun driver corrisponde usciamo dal programma con un messaggio di errore.

Trovato il driver dobbiamo settare la modalità video. Mediante il comando geDriver_GetNextMode scorriamo tutte le modalità messe a disposizione dal driver.

 

      //Get the first mode

      Mode = geDriver_GetNextMode(Driver, NULL);

 

      //Loop through the mode list and find the mode that matches the width and height of our variables

      while (1)

      {

            //If there is no mode, break the loop and exit the program

            if (!Mode)

            {

                  MessageBox(hWnd, "Could not find a valid mode!", "MinApp Error...", 48);

                  _exit(-1);

            }

 

            //Get the width and height of the mode

            geDriver_ModeGetWidthHeight(Mode, &tempwidth, &tempheight);

 

            //See if the width and height match the variables.  If it does, break the loop

            if (tempwidth == io.Width && tempheight == io.Height)

                  break;

 

            //If not, get the next mode

            Mode = geDriver_GetNextMode(Driver, Mode);

      }

 

Ogni modalità ottenuta viene confrontata con quelle impostate e si esce da ciclo quando si trova quella che corrisponde. Se non viene trovata nessuna corrispondenza si esce dal programma con un messaggio di errore.

 

      //We have the Driver and Mode we want, now tell the engine to start

      if (!geEngine_SetDriverAndMode(Engine, Driver, Mode))

      {

            MessageBox(hWnd, "Could not start the engine!", "MinApp Error...", 48);

            _exit(-1);

      }

}

 

Trovata la modalità video e il driver si imposta la scelta effettuata con il comando geEngine_SetDriverAndMode.

La funzione MainLoop()

Questa è la funzione che si occupa di effettuare il rendering dell'ambiente e di interagire con l'utente. Attualmente l'unica interazione avviene per terminare l'applicazione.

 

void MainLoop() {

      MSG msg;

      geBoolean run;

 

      //Main loop

      run = GE_TRUE;

 

      while (run) {

            if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) {    // Is There A Message Waiting?

                  if (msg.message==WM_QUIT) {                    // Have We Received A Quit Message?

                        run = 0;                                       // If So done=TRUE

                  } else {                                             // If Not, Deal With Window Messages

                        TranslateMessage(&msg);                  // Translate The Message

                        DispatchMessage(&msg);                   // Dispatch The Message

                  }

            } else {                                                   // If There Are No Messages

                  if (!geEngine_BeginFrame(Engine, Camera, GE_FALSE))

                        run = 0;

 

                  if (!geEngine_RenderWorld(Engine, World, Camera, 0.0f))

                        run = 0;

 

                  if (!geEngine_EndFrame(Engine))

                        run = 0;

 

                  if (io.keys[VK_ESCAPE]) {                                 

                        run=0;

                  }

            }

      }

}

 

La funzione consiste in un ciclo che dipende dalla variabile booleana "run". Finché il valore della variabile rimane true il ciclo continua. Attualmente un ciclo è composto da due parti.

La prima parte effettua una verifica dei messaggi in arrivo all'applicazione e verifica se è presente il messaggio WM_QUIT che windows manda quando una applicazione deve essere chiusa. In tal caso run viene posto a false e si esce dal ciclo.

La seconda parte effettua il rendering del World. Le operazioni di rendering possono essere effettuate solo tra le istruzioni di geEngine_BeginFrame e geEngine_EndFrame.

L'errata conclusione di una di queste operazioni o la pressione del tasto ESC fa uscire dal ciclo.

La funzione Shutdown()

Scopo di tale funzione è di rilasciare le risorse che sono state impegnate: World, Camera, SoundSys e Engine.

 

void Shutdown()

{

      if (World)

      {

            geEngine_RemoveWorld(Engine, World);

            geWorld_Free(World);

      }

 

      if (Camera)

            geCamera_Destroy(&Camera);

 

      if (SoundSys)

            geSound_DestroySoundSystem(SoundSys);

 

      if (Engine)

      {

            geEngine_ShutdownDriver(Engine);

            geEngine_Free(Engine);

      }

}

 

 

Conclusioni

Come si può vedere i comandi essenziali per l'esecuzione di una semplice applicazione dimostrativa "donothing" sono abbastanza pochi e piuttosto semplici da intuire. Nelle prossime lezioni vedremo come aggiungere interattività, permettendo alla telecamera di muoversi nell'ambiente mediante uso combinato di tastiera e mouse.


 

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da:
Luca Sabatucci